package beautiful.butterfly.server.httpserver.mvc.http;

import beautiful.butterfly.server.application.Application;
import beautiful.butterfly.server.httpserver.handler.HttpConst;
import beautiful.butterfly.server.httpserver.handler.HttpServer;
import beautiful.butterfly.server.httpserver.handler.SessionHandler;
import beautiful.butterfly.server.httpserver.kit.StringKit;
import beautiful.butterfly.server.httpserver.mvc.multipart.FileItem;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import io.netty.handler.codec.http.multipart.*;
import io.netty.util.CharsetUtil;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.URLConnection;
import java.nio.file.Files;
import java.util.*;


@Slf4j
public class HttpRequest implements Request {
    private static final HttpDataFactory HTTP_DATA_FACTORY = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); // Disk if size exceed

    static {
        DiskFileUpload.deleteOnExitTemporaryFile = true;
        DiskFileUpload.baseDirectory = null;
        DiskAttribute.deleteOnExitTemporaryFile = true;
        DiskAttribute.baseDirectory = null;
    }

    private HttpServer httpServer;
    private SessionHandler sessionHandler;
    private ByteBuf body = Unpooled.copiedBuffer("", CharsetUtil.UTF_8);

    private String uri;
    private String url;

    private String method;
    private boolean keepAlive;

    private Map<String, String> headerMap = null;
    private Map<String, Object> attributeMap = null;
    private Map<String, List<String>> parameterMap = new HashMap<>();
    private Map<String, String> pathParams = null;
    private Map<String, Cookies> nameToCookieMap = new HashMap<>();
    private Map<String, FileItem> fileNameToFileItemMap = new HashMap<>();

    public static HttpRequest build(ChannelHandlerContext channelHandlerContext, FullHttpRequest fullHttpRequest, HttpServer httpServer, SessionHandler sessionHandler) {
        HttpRequest httpRequest = new HttpRequest();
        httpRequest.httpServer = httpServer;
        httpRequest.url = fullHttpRequest.uri();
        int indexOf = httpRequest.url.indexOf('?');
        httpRequest.uri = indexOf < 0 ? httpRequest.url : httpRequest.url.substring(0, indexOf);
        //
        httpRequest.method = fullHttpRequest.method().name();
        httpRequest.keepAlive = HttpUtil.isKeepAlive(fullHttpRequest);
        httpRequest.init(fullHttpRequest);

        if (Application.developMode) {
            System.out.println("HttpHeaders:");
            HttpHeaders httpHeaders = fullHttpRequest.headers();
            List<Map.Entry<String, String>> list = httpHeaders.entries();
            for (Map.Entry<String, String> e : list) {
                System.out.println(e.getKey() + ":" + e.getValue());
            }
        }

        return httpRequest;
    }

    private void init(FullHttpRequest fullHttpRequest) {
        // headerMap
        HttpHeaders httpHeaders = fullHttpRequest.headers();
        if (httpHeaders.size() > 0) {
            this.headerMap = new HashMap<>(httpHeaders.size());
            for (Map.Entry<String, String> entry : httpHeaders.entries()) {
                headerMap.put(entry.getKey(), entry.getValue());
            }
        } else {
            this.headerMap = new HashMap<>();
        }

        // body
        this.body = fullHttpRequest.content().copy();

        //parameterMap
        Map<String, List<String>> parameterMap = new QueryStringDecoder(fullHttpRequest.uri(), CharsetUtil.UTF_8).parameters();
        if (null != parameterMap) {
            this.parameterMap = new HashMap<>();
            this.parameterMap.putAll(parameterMap);
        }
//parseInterfaceHttpData
        if (!HttpConst.METHOD_GET.equals(fullHttpRequest.method().name())) {
            HttpPostRequestDecoder httpPostRequestDecoder = new HttpPostRequestDecoder(HTTP_DATA_FACTORY, fullHttpRequest);
            Iterator<InterfaceHttpData> iterator = httpPostRequestDecoder.getBodyHttpDatas().iterator();
            while (iterator.hasNext()) {
                parseInterfaceHttpData(iterator.next());
            }

        }

        //parseCookie
        String cookie = header(HttpConst.COOKIE_STRING);
        if (cookie != null) {
            cookie = cookie.length() > 0 ? cookie : header(HttpConst.COOKIE_STRING.toLowerCase());
            if (StringKit.isNotBlank(cookie)) {
                Iterator<Cookie> iterator = ServerCookieDecoder.LAX.decode(cookie).iterator();
                while (iterator.hasNext()) {
                    parseCookie(iterator.next());
                }

            }
        }

    }


    private void parseInterfaceHttpData(InterfaceHttpData interfaceHttpData) {
        try {
            switch (interfaceHttpData.getHttpDataType()) {
                case Attribute:
                    Attribute attribute = (Attribute) interfaceHttpData;
                    String name = attribute.getName();
                    String value = attribute.getValue();
                    this.parameterMap.put(name, Collections.singletonList(value));
                    break;
                case FileUpload:
                    FileUpload fileUpload = (FileUpload) interfaceHttpData;
                    parseFileUpload(fileUpload);
                    break;
                default:
                    break;
            }
        } catch (IOException e) {
            log.error("parse getRequest parameter error", e);
        } finally {
            interfaceHttpData.release();
        }
    }

    private void parseFileUpload(FileUpload fileUpload) throws IOException {
        if (fileUpload.isCompleted()) {
            String contentType = StringKit.mimeType(fileUpload.getFilename());
            if (null == contentType) {
                contentType = URLConnection.guessContentTypeFromName(fileUpload.getFilename());
            }
            if (fileUpload.isInMemory()) {
                FileItem fileItem = new FileItem(fileUpload.getName(), fileUpload.getFilename(),
                        contentType, fileUpload.length());
                fileItem.setData(fileUpload.getByteBuf().array());
                fileNameToFileItemMap.put(fileItem.getName(), fileItem);
            } else {
                FileItem fileItem = new FileItem(fileUpload.getName(), fileUpload.getFilename(),
                        contentType, fileUpload.length());
                byte[] bytes = Files.readAllBytes(fileUpload.getFile().toPath());
                fileItem.setData(bytes);
                fileNameToFileItemMap.put(fileItem.getName(), fileItem);
            }
        }
    }


    private void parseCookie(io.netty.handler.codec.http.cookie.Cookie cookie) {
        Cookies Cookies = new Cookies();
        Cookies.name(cookie.name());
        Cookies.value(cookie.value());
        Cookies.httpOnly(cookie.isHttpOnly());
        Cookies.path(cookie.path());
        Cookies.domain(cookie.domain());
        Cookies.maxAge(cookie.maxAge());
        this.nameToCookieMap.put(Cookies.name(), Cookies);
    }

    @Override
    public String host() {
        return this.header("Host");
    }


    @Override
    public String getUri() {
        return this.uri;
    }

    @Override
    public String getUrl() {
        return this.url;
    }

    String userAgent() {
        return header(HttpConst.USER_AGENT);
    }


    @Override
    public Map<String, String> pathParams() {
        return this.pathParams;
    }

    @Override
    public String queryString() {
        return this.url;
    }

    /**
     * Get a URL parameter
     */
    String pathString(@NonNull String name) {
        return pathParams().get(name);
    }

    /**
     * Return a URL parameter for a Int type
     */
    Integer pathInt(@NonNull String name) {
        String val = pathString(name);
        return StringKit.isNotBlank(val) ? Integer.parseInt(val) : null;
    }

    /**
     * Return a URL parameter for a Long type
     *
     * @param name Parameter name
     * @return Return Long parameter value
     */
    Long pathLong(@NonNull String name) {
        String val = pathString(name);
        return StringKit.isNotBlank(val) ? Long.parseLong(val) : null;
    }

    @Override
    public Map<String, List<String>> parameterMap() {
        return parameterMap;
    }

    @Override
    public String method() {
        return this.method;
    }


    @Override
    public Session getSession() {
        return this.sessionHandler.createSession(this);
    }


    @Override
    public Map<String, String> getNameToCookieMap() {
        Map<String, String> nameToCookieMap = new HashMap<>(this.nameToCookieMap.size());
        for (String key :
                this.nameToCookieMap.keySet()) {
            nameToCookieMap.put(key, this.nameToCookieMap.get(key).value());
        }

        return nameToCookieMap;
    }


    @Override
    public Request cookie(@NonNull Cookies Cookies) {
        this.nameToCookieMap.put(Cookies.name(), Cookies);
        return this;
    }

    @Override
    public Map<String, String> getHeaderMap() {
        return this.headerMap;
    }

    @Override
    public boolean keepAlive() {
        return this.keepAlive;
    }

    @Override
    public Map<String, Object> getAttributeMap() {
        if (null == this.attributeMap) {
            this.attributeMap = new HashMap<>(4);
        }
        return this.attributeMap;
    }

    @Override
    public Map<String, FileItem> getFileNameToFileItemMap() {
        return fileNameToFileItemMap;
    }

    @Override
    public ByteBuf body() {
        return this.body;
    }

    @Override
    public String bodyToString() {
        return this.body.toString(CharsetUtil.UTF_8);
    }


    public String query(@NonNull String name) {
        List<String> list = parameterMap().get(name);
        if (null != list && list.size() > 0)
            return list.get(0);
        return null;
    }


    String query(@NonNull String name, @NonNull String defaultValue) {
        String value = query(name);
        if (value != null) {
            return value;
        }
        return defaultValue;
    }

    Integer queryInt(@NonNull String name) {
        String value = query(name);
        if (value != null) {
            return Integer.parseInt(value);
        }
        return null;
    }

    int queryInt(@NonNull String name, int defaultValue) {
        String value = query(name);
        if (value != null) {
            return Integer.parseInt(value);
        }

        return defaultValue;
    }


    Long queryLong(@NonNull String name) {
        String value = query(name);
        if (value != null) {
            return Long.parseLong(value);
        }

        return null;
    }


    long queryLong(@NonNull String name, long defaultValue) {
        String value = query(name);
        if (value != null) {
            return Long.parseLong(value);
        }
        return defaultValue;
    }


    boolean isAjax() {
        return "XMLHttpRequest".equals(header("x-requested-with"));
    }


    public String cookie(@NonNull String name) {
        String value = getNameToCookieMap().get(name);
        if (value != null) {
            return value;
        }
        return null;
    }


    public String header(@NonNull String name) {
        return getHeaderMap().get(name);
    }


    String header(@NonNull String name, @NonNull String defaultValue) {
        String value = header(name);
        return value.length() > 0 ? value : defaultValue;
    }


    public Request setAttribute(@NonNull String name, Object value) {
        if (null != value) getAttributeMap().put(name, value);
        return this;
    }

    <T> T setAttribute(String name) {
        if (null == name) return null;
        Object object = getAttributeMap().get(name);
        return null != object ? (T) object : null;
    }

    FileItem getFileItem(@NonNull String fileName) {
        return getFileNameToFileItemMap().get(fileName);
    }


    String cookie(@NonNull String name, @NonNull String defaultValue) {
        String value = cookie(name);
        if (value != null) {
            return value;
        }
        return defaultValue;
    }

    public Object getAttribute(String name) {
        return null;
    }
}